iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0

型別概述

Rust 基本型別有6種,又可以再分成2種原生資料型別子集:純量(scalar)複合(compound)
純量:整數(integer)浮點數(floating-point)布林(boolean)以及字元(character),只會有一個數值。
複合:元組(tuples)陣列(arrays),數個數值的集合。

和 JavaScript 和 Python 比起來會發現基本型別裡沒有空值(null 或 None),這和 Rust 的核心理念:內存安全和並發安全有關。

整數型別介紹

先看整數的部分,Rust 內建的整數型別如下。

長度 帶號 非帶號
8 位元 i8 u8
16 位元 i16 u16
32 位元 i32 u32
64 位元 i64 u64
128 位元 i128 u128
系統架構 isize usize

簡單看可以分成兩部分,iu表示是否帶正負符號,後面則是表示有多少位元(bit)可以用來儲存。
帶正負的 i (signed)開頭系列可以存的數字從-(2ⁿ⁻¹) ~ 2ⁿ⁻¹-1u(unsigned)就是 0 ~ 2ⁿ-1

i8 舉例,代表有 8 個位元儲存帶正負符號的數字,紀錄範圍就是 -2⁷ ~ 2⁷-1 也就是 -128~127, u8 的話則是 0~255。

isize 與 usize 型別則是依據你程式運行的電腦架構來決定大小,例如 64 位元架構上的話就是 64 位元,會用到的情況包括:

  • 指針和陣列索引
  • 內存相關操作

之後如果有遇到這些情境再來看。

回到整數型別,標示上有一些小訣竅。
我們直接寫在程式碼裡面的數字稱為數字字面值(number literals)。

  • 符合多種數字型別的數字字面值能用尾綴的方式加上型別,比如說用57u8來指定型別。
  • 數字字面值也可以加上底線_分隔方便閱讀,比如說1_000其實就和指定1000的數值一樣。

其他標示的方式如官網所示:

數字字面值 範例
十進制 98_222
十六進制 0xff
八進制 0o77
二進制 0b1111_0000
位元組(僅限u8 b'A'

測試

和 JavaScript 比起來更細的整數型別可以有效地利用空間,不過可以選更小的數字範圍就代表數字更容易超過這個範圍了,那超過的話會發生什麼事呢?

先看一開始賦值就超過範圍的話:

fn main() {
    println!("Hello");
    let num = 250i8;
    println!("{}", num);
}
$ cargo run
   Compiling types_int v0.1.0 (/Users/lanshihchun/Desktop/rust/types_int)
error: literal out of range for `i8`
 --> src/main.rs:3:15
  |
3 |     let num = 250i8;
  |               ^^^^^
  |
  = note: the literal `250i8` does not fit into the type `i8` whose range is `-128..=127`
  = help: consider using the type `u8` instead
  = note: `#[deny(overflowing_literals)]` on by default

error: could not compile `types_int` (bin "types_int") due to 1 previous error

可以看到在編譯過程就會報錯,而且會有個滿明確的錯誤訊息。

那如果是經過操作才超過呢?

fn main() {
    println!("Hello");
    let mut num = 250u8;
    num += 20;
    println!("{}", num);
}
$ cargo run
   Compiling types_int v0.1.0 (/Users/lanshihchun/Desktop/rust/types_int)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/types_int`
Hello
thread 'main' panicked at src/main.rs:4:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

可以看到有編譯成功,在 run time 的時候才出錯,這是因為我們目前編譯是用 debug 模式,如果改成 release 模式輸出的話結果會不同,在cargo run 多加上 --release再試一次:

$ cargo run --release
   Compiling types_int v0.1.0 (/Users/lanshihchun/Desktop/rust/types_int)
    Finished `release` profile [optimized] target(s) in 0.42s
     Running `target/release/types_int`
Hello
14

可以看到除了編譯成功以外,run time 也執行完成並且沒有任何錯誤訊息,最重要的是,結果很明顯和我們想要的不一樣,這就是所謂的 整數溢位(Integer Overflow) ,簡單的說就是會超過的部分會從頭繼續往下走,以上面的例子來說 u8 最多可以存到 255,我們預期的結果是 250 + 20 = 270 ,超過的部分回到 0 開始,所以 256 的結果變成 0,270 的結果就變成 14 了。

必須要非常小心兩種模式的行為不同,即使測試環境會報錯,一旦到正式環境如果沒適當處理就很難被發現(不會報錯),所以在定義型別的時候一定要謹慎選擇合適的大小(Rust 預設為 i32),否則最慘的情況會是造成髒資料又沒辦法在第一時間發現及修正,如果遇到像算金額之類,難以想像會造成多大的損失。

處理整數溢位

也因此 Rust 提供了多種方法來顯式處理可能的溢位,包括 wrapping_*checked_*overflowing_* 和 saturating_* 方法,可以根據需求來處理溢位情況。
以下是各種方法的範例:

1. 使用 wrapping_* 方法 (default)

wrapping_* 方法會在發生溢位時繞回(wrap around),例如加法運算的結果超過類型的最大值時,會回到類型的最小值。沒指定的話就是 Rust 的預設行為。

fn main() {
    let a: u8 = 255;
    let b: u8 = 1;

    let result = a.wrapping_add(b);
    println!("Wrapping add: {} + {} = {}", a, b, result);
}
$ cargo run --release
Wrapping add: 255 + 1 = 0

2. 使用 checked_* 方法

checked_* 方法在發生溢位時會返回 None,否則返回 Some(結果)

fn main() {
    let a: u8 = 255;
    let b: u8 = 1;

    match a.checked_add(b) {
        Some(result) => println!("Checked add: {} + {} = {}", a, b, result),
        None => println!("Checked add: Overflow occurred"),
    }
}
$ cargo run --release
Checked add: Overflow occurred

3. 使用 overflowing_* 方法

overflowing_* 方法返回一個元組 (結果, bool),其中 bool 表示是否發生溢位。

fn main() {
    let a: u8 = 255;
    let b: u8 = 1;

    let (result, overflow) = a.overflowing_add(b);
    println!("Overflowing add: {} + {} = {}, Overflow: {}", a, b, result, overflow);
}
$ cargo run --release
Overflowing add: 255 + 1 = 0, Overflow: true

4. 使用 saturating_* 方法

saturating_* 方法在溢位時會返回類型的最大值或最小值。
超過最大值的情況:

fn main() {
    let a: u8 = 255;
    let b: u8 = 1;

    let result = a.saturating_add(b);
    println!("Saturating add: {} + {} = {}", a, b, result);
}
$ cargo run --release
Saturating add: 255 + 1 = 255

小於最小值的情況:

fn main() {
    let a: u8 = 255;
    let b: u8 = 1;

    let result = b.saturating_sub(a);
    println!("Saturating sub: {} - {} = {}", b, a, result);
}
$ cargo run --release
Saturating sub: 1 - 255 = 0

結語

Rust 在整數型別的處理上比我之前碰過的程式語言有更精細的控制,透過不同位元長度和符號的選擇,可以更靈活地運用內存空間,可以理解為何 Rust 在開發性能要求高的應用會更具優勢。
另外內建了多種方式來處理溢位情況,看得出 Rust 對安全性的重視。


上一篇
Day3 - 從 Echo Function 接觸基本概念
下一篇
Day5 - 型別:浮點數以及數值運算
系列文
螃蟹幼幼班:Rust 入門指南25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言